﻿//************************************************************************************************
// Copyright © 2003-2012 Steven M. Cohn. All Rights Reserved.
//
// Facility:	Microsoft Development Environment 2005  Version 8.0.40607.16 (Beta)
// Environment:	Microsoft .NET Framework 2.0            Version 2.0.40607
//
// Description: Provides an enumerable stream of disk-based cookie files
//
// You can find a user's cookies in two locations:
// 
//  1. C:\Users\NAME\Cookies
//  2. C:\Users\NAME\AppData\Roaming\Microsoft\Windows\Cookies
//
// But the first is a symlink to the second so we only need to look in either one.
// We also need to look in C:\Users\NAME\AppData\Local\Temp\Cookies
//
// Revision History:
// -Who--------------- -When---------- -What------------------------------------------------------
// Steven M. Cohn		18-Oct-2003		New
// Steven M. Cohn		12-Oct-2004		VS.NET 2005, .NET 2.0
// Steven M. Cohn		20-Nov-2012		Refresh! VS2010 .NET 4.5 64-bit
//************************************************************************************************

namespace CookieChipper
{
	using System;
	using System.Globalization;
	using System.IO;


	/// <summary>
	/// Provides access to an individual cookie file.
	/// </summary>

	internal class CookieFileProvider : IDisposable
	{
		private const string DateFormat = "MM/dd/yyyy h:mm tt";

		private string path;
		private StreamReader reader;
		private bool isDisposed = false;


		//========================================================================================
		// Lifecycle
		//========================================================================================

		/// <summary>
		/// Initialize a new provider for the given file path.
		/// </summary>
		/// <param name="path">A string specifying the full path of the cookie file.</param>

		public CookieFileProvider (string path)
		{
			if (!File.Exists(path))
			{
				throw new FileNotFoundException(path);
			}

			try
			{
				reader = File.OpenText(path);
			}
			catch
			{
				reader = null;
				throw;
			}

			this.path = path;
			this.isDisposed = false;
		}


		/// <summary>
		/// Dispose the instance.
		/// </summary>

		public void Dispose ()
		{
			Dispose(true);
		}


		/// <summary>
		/// Dispose the instance.  Can be overriden by inheritors, which should
		/// invoke base.Dispose(disposing).
		/// </summary>
		/// <param name="disposing"></param>

		protected virtual void Dispose (bool disposing)
		{
			if (!isDisposed)
			{
				if (reader != null)
				{
					reader.Dispose();
					reader = null;
				}

				isDisposed = true;
			}
		}


		//========================================================================================
		// Methods
		//========================================================================================

		/// <summary>
		/// Delete the specified cookie file.
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>

		public static bool Delete (string path)
		{
			try
			{
				if (File.Exists(path))
				{
					var attributes = File.GetAttributes(path);
					if (attributes.HasFlag(FileAttributes.ReadOnly))
					{
						File.SetAttributes(path, attributes ^ FileAttributes.ReadOnly);
					}

					File.Delete(path);
				}
			}
			catch
			{
				return false;
			}

			return true;
		}


		/// <summary>
		/// Load the information available in the file and store it as Chips in the given cookie.
		/// </summary>
		/// <param name="cookie"></param>
		/// <returns></returns>

		public bool LoadChips (Cookie cookie)
		{
			Chip chip;
			while ((chip = ReadRecord()) != null)
			{
				if (String.IsNullOrEmpty(cookie.Domain))
				{
					cookie.Domain = chip.Domain;
				}

				cookie.Add(chip);
			}

			return true;
		}


		#region Record layout
		/*
		 * Cookie files may contain multiple cookies.  Each cookie consists of eight
		 * items, one per line.  The follow example shows two cookies:
		 *
		 * myCookieA							; 0 name
		 * popup_ad								; 1 value
		 * www.theonion.com/onion3806/			; 2 domain/path
		 * 1088									; 3 flags?  [secure | hasKeys]
		 * 3596769152							; 4 expiration dttm 1
		 * 29473513								; 5 expiration dttm 2
		 * 2888195648							; 6 creation dttm 1
		 * 29473312								; 7 creation dttm 2
		 * *
		 * myCookieB
		 * popup_ad_under
		 * www.theonion.com/onion3806/
		 * 1088
		 * 2286834560
		 * 29473515
		 * 1584661056
		 * 29473314
		 * *
		 *
		 * Note the two cookies are separated by a line containing an asterisk.  The
		 * last asterisk in the file is optional.
		 *
		 * The domain/path (index 2) contains both the domain and the path.  The domain
		 * in always ends with the slash (www.theonion.com/).  The path follows.
		 *
		 * The flags (index 3) may be a security flag, 1024 and 1088 have been observed.
		 *
		 * The date/time stamps are split into two longwords, with the low order longword
		 * first and the high order longword second.  These make up a quadword specifying
		 * the date and time as a FileTime format.  We can easily use the .NET
		 * DateTime.FromFileTime() method to convert this quadword to a DateTime; this
		 * method handles UTC offsets and timezones.
		 */
		#endregion Record layout

		/// <summary>
		/// Reads the cookie file returning a single Chip.
		/// </summary>
		/// <returns>
		/// A cookie Chip of available or null of there are no more to read in the cookie file.
		/// </returns>

		private Chip ReadRecord ()
		{
			Chip chip = null;

			string line;
			string loword = null;
			int counter = 0;

			while (((line = reader.ReadLine()) != null) && (counter < 8))
			{
				switch (counter)
				{
					case 0:
						chip = new Chip(line);
						break;

					case 1:
						chip.Value = line;
						break;

					case 2:
						chip.Domain = line.EndsWith("/") ? line.Substring(0, line.Length - 1) : line;
						break;

					case 3:
						chip.Flags = (ChipFlags)uint.Parse(line);
						break;

					case 4:
						// low longword of expiration time
						loword = uint.Parse(line).ToString("X8");
						break;

					case 5:
						// concat quadword and translate
						chip.Expires = DateTime.FromFileTime(
							long.Parse(
								uint.Parse(line).ToString("X8") + loword,
								NumberStyles.HexNumber)
							);

						DateTime now = DateTime.Now;
						if (!chip.Expires.Equals(DateTime.MinValue) &&
							(now.Subtract(chip.Expires).Days > 0))
						{
							// expired
							chip.Flags |= ChipFlags.Expire;
						}

						break;

					case 6:
						// low longword of creation time
						loword = uint.Parse(line).ToString("X8");
						break;

					case 7:
						// concat quadword and translate
						chip.Created = DateTime.FromFileTime(
							long.Parse(
								uint.Parse(line).ToString("X8") + loword,
								NumberStyles.HexNumber)
							);
						break;
				}

				counter++;
			}

			// validate we have a complete record
			return counter < 8 ? null : chip;
		}


		//========================================================================================
		// Save()
		//========================================================================================

		public static bool Save (Cookie cookie)
		{
			if (File.Exists(cookie.Path))
			{
				if (!Delete(cookie.Path))
				{
					return false;
				}
			}

			using (StreamWriter writer = File.CreateText(cookie.Path))
			{
				foreach (Chip chip in cookie.Values)
				{
					writer.WriteLine(chip.Name);
					writer.WriteLine(chip.Value);

					string domain = chip.Domain;
					if (!domain.EndsWith("/"))
					{
						domain += "/";
					}

					writer.WriteLine(domain);

					writer.WriteLine((int)chip.Flags);

					long filetime = chip.Expires.ToFileTime();
					writer.WriteLine((filetime & 0x00000000FFFFFFFF).ToString());
					writer.WriteLine(((filetime ^ 0x00000000FFFFFFFF) >> 32).ToString());

					filetime = chip.Created.ToFileTime();
					writer.WriteLine((filetime & 0x00000000FFFFFFFF).ToString());
					writer.WriteLine(((filetime ^ 0x00000000FFFFFFFF) >> 32).ToString());

					writer.WriteLine("*");
				}
			}

			return true;
		}


		#region Unit testing

		/// <summary>
		/// Get the directory path of the cookie cache.
		/// </summary>

		public string _Path
		{
			get { return path; }
		}

		#endregion Unit testing
	}
}
